package openjdk.stream;

import java.util.*;
import java.util.stream.Stream;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.*;

public class GroupDemo {

    public static void main(String[] args) {
        group();
        //group01();
        //group02();
        //group03();
        //group04();
        //group05();
        //group05_1();
        //group666();
    }


    private static void group() {
        Stream<String> words = Stream.of("Java", "Magazine", "is",
                "the", "best");

        Map<String, Long> letterToCount = words.map(w -> w.split(""))
                .flatMap(Arrays::stream)
                .collect(groupingBy(identity(), counting()));
        System.out.println(letterToCount);
    }


    public enum Country {POLAND, UK, GERMANY}


    static class Student {

        private final String name;
        private final int age;
        private final Country country;
        private final int score;

        public Student(String name, int age, Country country, int score) {
            this.name = name;
            this.age = age;
            this.country = country;
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        public Country getCountry() {
            return country;
        }

        public int getScore() {
            return score;
        }


        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", country=" + country +
                    ", score=" + score +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Student)) return false;
            Student student = (Student) o;
            return age == student.age &&
                    score == student.score &&
                    Objects.equals(name, student.name) &&
                    country == student.country;
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age, country, score);
        }
    }

    static List<Student> students = Arrays.asList(
            /*                 NAME          AGE    COUNTRY        SCORE */
            new Student("Jan", 13, Country.POLAND, 92),
            new Student("Anna", 15, Country.POLAND, 95),
            new Student("Helga", 14, Country.GERMANY, 93),
            new Student("Helga", 14, Country.GERMANY, 93),
            new Student("Leon", 14, Country.GERMANY, 97),
            new Student("Michael", 14, Country.UK, 90),
            new Student("Tim", 15, Country.UK, 91),
            new Student("George", 14, Country.UK, 98)
    );


    /**
     * grouping students by country
     * <p>
     * Output:
     * {UK=[Michael/14/UK/90, Tim/15/UK/91, George/14/UK/98],
     * POLAND=[Jan/13/POLAND/92, Anna/15/POLAND/95],
     * GERMANY=[Helga/14/GERMANY/93, Leon/14/GERMANY/97]}
     */
    private static void group01() {
        Map<Country, List<Student>> studentsByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry));
        System.out.println(studentsByCountry);
    }

    /**
     * By default elements with the same key are gathered into List<T>,
     * <p>
     * <p>
     * we may change this behaviour by specifying second parameter to groupingBy,
     * for example:
     */
    private static void group02() {
        // Use Set's as "buckets"
        Map<Country, Set<Student>> studentsByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry, toSet()));
        System.out.println(studentsByCountry);

        // Use ArrayList's as "buckets" (The same with group01)
        Map<Country, ArrayList<Student>> studentsByCountryAL =
                students.stream()
                        .collect(groupingBy(Student::getCountry,
                                toCollection(ArrayList::new)));
        System.out.println(studentsByCountryAL);
    }

    /**
     * By default elements with the same key are gathered into List<T>,
     * <p>
     * <p>
     * we may change this behaviour by specifying second parameter to groupingBy,
     * for example:
     */
    private static void group03() {
        Map<Country, Long> numberOfStudentsByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry, counting()));

        System.out.println(numberOfStudentsByCountry);

    }

    /**
     * what about average student score per country? No problem with that:
     */
    private static void group04() {
        Map<Country, Double> avgScoreByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry,
                                averagingInt(Student::getScore)));

        System.out.println(avgScoreByCountry);
    }

    /**
     * compute max student age per country:
     * <p>
     * select max(age) from table group by country.
     * <p>
     * we use mapping(mapper, downstream) collector that transforms every element passed to it
     * using mapper function and then passes that transformed element to downstream collector.
     * So what we do here is that we first group students by country, then in every group we map
     * every student to his/her age and we pass that age to maxBy() collector that finally computes
     * max age in every group.
     */
    private static void group05() {
        Map<Country, Optional<Integer>> maxAgeByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry,
                                mapping(Student::getAge,
                                        maxBy(Integer::compare))));
        System.out.println(maxAgeByCountry);
    }

    /**
     * maxBy() collector returns Optional<T> to signify that sometimes it cannot compute maximum
     * e.g.
     * it happens when we use maxBy() on empty stream. We may fix problem with optionals if we map
     * maxBy() result via Optional::get collectingAndThen() collector does exactly that:
     */
    private static void group05_1() {
        Map<Country, Integer> maxAgeByCountry =
                students.stream()
                        .collect(groupingBy(Student::getCountry,
                                mapping(Student::getAge,
                                        collectingAndThen(maxBy(Integer::compare), Optional::get))));

        System.out.println(maxAgeByCountry);
    }

    /**
     * groupingBy country and age.
     * <p>
     * select country, age ,* from table group by country,age
     * <p>
     * Since we may pass any collectors as a second argument to groupingBy, We may pass another
     * groupingBy collector - this way we may group stream element by more than one criteria.
     */
    private static void group666() {
        Map<Country, Map<Integer, List<Student>>> studentsGroupingByCountryAndAge =
                students.stream().collect(groupingBy(Student::getCountry, groupingBy(Student::getAge)));

        System.out.println(studentsGroupingByCountryAndAge);
    }

}
